﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Eventing.Reader;
using System.Management.Automation;
using System.Threading;
using network;
using shell;

namespace HpcClusterController
{
    class Controller
    {
        private UDPclient clientUDP;
        private Shell shell;
        private HashSet<String> reservedNodes;

        private List<PSObject> queue;

        System.Diagnostics.EventLog logger;

        private void log(string str)
        {
            logger.Source = "Controller";
            logger.WriteEntry(str);
        }

        public Controller(string nic, System.Diagnostics.EventLog eLog)
        {
            this.logger = eLog;

            try
            {
                this.shell = new Shell("Add-PSSnapIn Microsoft.HPC");
            }
            catch (Exception e)
            {
                log("Error creating Shell: " + e.Message);
            }
            try
            {
                this.clientUDP = new UDPclient(nic);
            }
            catch (Exception e)
            {
                log("Error getting NIC: " + e.Message);
            }


            this.reservedNodes = new HashSet<string>();
            this.queue = new List<PSObject>();
        }

        private void updateReservedSet(string nodeName)
        {
            Collection<PSObject> results = shell.run(
                    "get-hpcnode -Name {0} | %{{ $_.MachineGuid }}",
                    nodeName);

            reservedNodes.Remove(results[0].ToString());
        }

        void sendRequest(String msg)
        {
            int times = 5000;
            bool first = true;

            log("Request sent:" + msg);

            while (true)
            {
                try
                {
                    clientUDP.sendMsg(msg);
                }
                catch (AckTimeout)
                {
                    log("Request timeout");
                    if (first) // the first time, we call Socorro
                    {
                        Socorro.call();
                        first = false;
                    }
                    Thread.Sleep(times);
                    times = times * 2;
                    continue;
                }

                // if we're here, all went ok.
                break;
            }
        }

        public void watcher_EventRecordWritten(object sender, EventRecordWrittenEventArgs e)
        {
            log(String.Format("Event: {0}, Job: {1}", e.EventRecord.Id, e.EventRecord.Properties[0].Value));
            //log("Evento: {0}, Job: {1}", e.EventRecord.Id, e.EventRecord.Properties[0].Value);

            EventRecord data = e.EventRecord;

            switch (data.Id)
            {
                case 0:
                    string jobID = data.Properties[0].Value.ToString();
                    jobCreated(jobID);
                    break;
                case 1:
                case 2:
                case 4:
                    jobEnded();
                    break;
                case 13:
                    string node = data.Properties[0].Value.ToString();
                    updateReservedSet(node);
                    break;
            }
        }

        private void jobCreated(string jobID)
        {
            Collection<PSObject> jobInfo = shell.run(
                "Get-HPCJob {0}",
                jobID); // argh! we need a method that returns a string
            
            string state = jobInfo[0].Properties["State"].Value.ToString();
 
            // POG!
            int minCores = (int)jobInfo[0].Properties["MinCores"].Value;
            int minNodes = minCores / 2;

            if (state == "Queued")
            {
                Collection<PSObject> offNodes = shell.run(
                    "get-hpcnode -State Online -Health Unreachable " +
                    " | %{{ $_.MachineGuid }}");

                Collection<PSObject> idleNodes = shell.run(
                    "Get-HPCNode -State Online -health OK" +
                    " | where {{ (Get-HPCMetricValue -node $_ -name HPCJobsRunning).value -eq 0 }}" +
                    " | %{{ $_.MachineGuid }}");

                if (idleNodes.Count + offNodes.Count - reservedNodes.Count >= minNodes)
                {
                    foreach (var m in idleNodes)
                    {
                        reservedNodes.Add(m.ToString()); // m = machineguid
                        minNodes -= 1; // update job demand

                        if (minNodes == 0)
                        {
                            break; // we don't need more
                        }

                    }
                    // now, if we still need machines, we use the offline set.
                    // the minNode is updated
                    if (minNodes > 0)
                    {
                        requestWake(offNodes, minNodes);
                    }
                }
                else
                {
                    queue.Add(jobInfo[0]);
                }
            }
        }

        private void jobEnded()
        {

            Thread.Sleep(2000);

            Collection<PSObject> offNodes, idleNodes;

            try
            {
                offNodes = shell.run(
                    "get-hpcnode -State Online -Health Unreachable " +
                    " | %{{ $_.MachineGuid }}");
            }
            catch (ShellException)
            {
                offNodes = new Collection<PSObject>();
            }
            catch (Exception e)
            {
                offNodes = new Collection<PSObject>();
                log(String.Format("Error getting Cluster Information:\n{0}",e.Message));
            }
            
            try 
	        {	        
		        idleNodes = shell.run(
                "Get-HPCNode -State Online -health OK" +
                " | where {{ (Get-HPCMetricValue -node $_ -name HPCJobsRunning).value -eq 0 }}" +
                " | %{{ $_.MachineGuid }}");
	        }
            catch (ShellException)
	        {
		        idleNodes = new Collection<PSObject>();
	        }
            catch (Exception e)
            {
                idleNodes = new Collection<PSObject>();
                log(String.Format("Error getting Cluster Information:\n{0}", e.Message));
            }

            PSObject job = getJobFromQueue(idleNodes.Count + offNodes.Count);

            if (job == null)
            {
                // no job can run, so we'll shutdown the idle nodes
                requestDown();
                return;
            }

            int minNodes = (int)job.Properties["MinCores"].Value / 2;
            requestWake(offNodes, minNodes - idleNodes.Count);

        }

        void requestWake(Collection<PSObject> nodes, int n)
        {
            // join the n first non reserved results
            String msg = String.Format("{0}", (int)MsgType.START_VM);
            int used = 0;

            foreach (var item in nodes)
            {
                string nodeGuid = item.ToString(); // MachineGuid
                if (reservedNodes.Add(nodeGuid))
                {
                    // we reserve the nodes we choose.
                    msg = msg + " " + nodeGuid;
                    if (++used == n) break;
                }
            }

            this.sendRequest(msg);
        }

        void requestDown()
        {
            Collection<PSObject> idleNodes = shell.run(
                "Get-HPCNode -State Online -health OK" +
                " | where {{ $_.Groups -NotMatch 'HeadNodes'}}" +
                " | where {{ (Get-HPCMetricValue -node $_ -name HPCJobsRunning).value -eq 0 }}" +
                " | %{{ $_.MachineGuid }}");

            if (idleNodes.Count == 0)
            {
                log("No node to stop.");
                //log("Nenhum nó pra desligar");
                return;
            }

            String msg = String.Format("{0}", (int)MsgType.SAVE_VM);
            foreach (var item in idleNodes)
            {
                string nodeGuid = item.ToString();
                msg = msg + " " + nodeGuid;
            }

            this.sendRequest(msg);
        }

        private PSObject getJobFromQueue(int n)
        {
            PSObject job = null;
            for (int i = 0; i < queue.Count; i++)
            {
                // TODO: UnitType
                if ((int)queue[i].Properties["MinNodes"].Value <= n)
                {
                    job = queue[i];
                    queue.RemoveAt(i);
                    break;
                }
            }

            return job;

        }

    }
}
